from selenium.common.exceptions import TimeoutException
from selenium import webdriver

# 这个暂时没用上...
import urllib.request
import urllib.parse

import traceback
import sys, os
import time
import subprocess
import socket

#selenium 官方文档：
#https://github.com/fool2fish/selenium-doc/blob/master/official-site/selenium-web-driver.md


def PrintException(Exception, e):
	""" 打印异常 """
	print("str(Exception):\t" + str(Exception) )
	print( "str(e):\t\t" + str(e) )
	print( "repr(e):\t", repr(e) ) 
	#print( "e.message:\t" + e.message
	#print( "traceback.print_exc():" + traceback.print_exc() )
	print( "traceback.format_exc():\n" + traceback.format_exc() )
	
	if repr(e) == "NoSuchElementException()":
		print("The cause of the problem may be the wait time is too short.")
		print("Please recall Method: WebCrawlerTool.SetWaitTime(), and set longer wait time.")


def getRemoteFileSize(url, proxy=None):
	""" 
		通过content-length头获取远程文件大小
		url - 目标文件URL
		proxy - 代理  
	"""
	opener = urllib.request.build_opener()
	if proxy:
		if url.lower().startswith('https://'):
			opener.add_handler(urllib.request.ProxyHandler({'https' : proxy}))
		else:
			opener.add_handler(urllib.request.ProxyHandler({'http' : proxy}))
	try:
		request = urllib.request.Request(url)
		request.get_method = lambda: 'HEAD'
		response = opener.open(request)
		response.read()
	except Exception:
		return 0
	else:
		print(response.headers)
		fileSize = dict(response.headers).get('Content-Length', 0)
		return int(fileSize)


""" 
所有可能用到的 Content-type 后缀 
Content-type 请参考网页：

http://www.w3school.com.cn/media/media_mimeref.asp
https://tool.oschina.net/commons

"""
gContentTypeDic = {
	"zip" : "application/x-zip-compressed",
	"rar" : "application/octet-stream",
	"doc" : "application/msword",
	"docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
	"ppt" : "application/vnd.ms-powerpoint",
	"pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation",
	"mp4" : "video/mpeg4",
}


class LoginInfo:
	""" 【登陆所需信息】 \\
	使用 Selenium 登陆 \\
	需要分别手动指定 [用户名] [密码] [登陆按钮] 分别对应的 [表单元素] 名称\\
	在调用登陆函数之前，需要先打开浏览器的"开发者工具"(F12 呼出) \\
	使用 "开发者工具" 左上角的 "选择器"，\\
	选择到表单元素进行模拟输入和点击按钮提交，\\
	来确定它们分别对应的 [表单元素] 名称 \\
	\\
	登陆没法确定返回值，请务必在手动操作，确认能成功登陆后，再调用此函数 !!!
	
	:成员:
	- loginUrl:    登陆页面的 URL
	- elemUN:      用户名对应的[表单元素]名称
	- elemPWD:     密码对应的[表单元素]名称
	- elemSUBMIT:  登陆按钮对应的[表单元素]名称
	- userName:    用户名
	- passWord:    登陆密码

	"""
	def __init__(self):
		self.loginUrl = ""
		self.elemUN = ""
		self.elemPWD = ""
		self.elemSUBMIT = ""
		self.userName = ""
		self.passWord = ""

	def IsOK(self):
		if \
		self.loginUrl == "" or \
		self.elemUN == "" or \
		self.elemPWD == "" or \
		self.elemSUBMIT == "" or \
		self.userName == "" or \
		self.passWord == "":
			return False
		else:
			return True


class SeleniumCrawler:
	""" 通用 Selenium 爬虫工具类.
	
	注意：
	由于此工具是通过浏览器后台运行获取数据
	所以必须预先安装Web浏览器，每一种浏览器的安装方法不尽相同
	构造函数也必须指定使用哪一种浏览器

	方法:
		SetWaitTime():	设置打开一次浏览器等待时长
	"""
	
	""" 爬虫所需的浏览器类型 """
	__browser_type = ''
	""" 爬虫所需的浏览器对象 """
	__browser = None
	""" 浏览器配置对象 """
	__profile = None
	""" 是否采用headless模式，默认是，会在后台模式下运行 """
	__headless = True
	""" 打开浏览器后等待数据时长，单位：秒 """
	__waitsec = 2
	""" 当前浏览器指定的下载路径 """
	__download_dir = ""
	""" 设置输出的日志文件名 """
	__logfilename = "SeleniumCrawler.log"


	def __init__(self):
		self.__browser_type = ''
		self.__browser = None
		self.__profile = None
		self.__headless = True
		self.__waitsec = 2
		self.__download_dir = ""
		self.__logfilename = "SeleniumCrawler.log"

		if os.path.exists(self.__logfilename):
			os.remove(self.__logfilename)



	def __del__(self):
		if self.__browser is not None:
			#关闭当前浏览器窗口
			self.__browser.close()
			self.__browser = None


	def WriteLogToFile(self, _info, _filename, _bPrint=True, _bTime=True):
		""" 
		输出信息到日志
		
		:参数:
		- _info: 要记录的日志信息
		- _filename: 输出到哪个日志文件
		- _bPrint: 是否需要打印到控制台
		- _bTime: 是否需要记录时间
		"""

		if os.path.exists(_filename):
			os.remove(_filename)

		log_info = ""
		if _bTime:
			str_time = time.strftime( '%Y-%m-%d %H:%M:%S', time.localtime(time.time()) )
			log_info += "[ " + str_time + " ]\n"
		log_info += _info

		# a：追加模式
		outputstream = open(_filename, "a")
		outputstream.write(log_info)
		outputstream.close()

		if _bPrint:
			print(log_info)


	def AddLogToFile(self, _info, _filename, _bPrint=True, _bTime=True):
		""" 
		追加信息到日志

		:参数:
		- _info: 要记录的日志信息
		- _filename: 输出到哪个日志文件
		- _bPrint: 是否需要打印到控制台
		- _bTime: 是否需要记录时间
		"""

		log_info = ""
		if _bTime:
			str_time = time.strftime( '%Y-%m-%d %H:%M:%S', time.localtime(time.time()) )
			log_info += "[ " + str_time + " ]\n"
		log_info += _info

		# a：追加模式
		outputstream = open(_filename, "a", encoding='utf-8', errors='ignore')
		outputstream.write(log_info)
		outputstream.close()

		if _bPrint:
			print(log_info)


	def NewProfile_Firefox(self, downloadDir=os.getcwd()):
		""" 创建一个新的 Profile 文件 """
		global gContentTypeDic

		# 如果发现调用 set_preference() 后不生效，请先将浏览器 Headless 标志设置为 False，
		# 即浏览器设置为有界面的模式，然后在调试出的 Firefox 浏览器上，新开一个标签页，
		# 然后在地址栏输入“ about:config” ，搜索对应的属性，查看是否生效
		# 参照：https://blog.csdn.net/ywyxb/article/details/65628329
		new_profile = webdriver.FirefoxProfile()

		# 0 --- 下载到桌面，1 --- 下载到浏览器默认下载路径，2 --- 下载到自定义目录
		# 设置为 2 的时候，设置自定义路径的就要去掉
		new_profile.set_preference("browser.download.folderList", 2)
		
		# 下载到任意指定目录，记得分隔符是两个反斜杠。os.getcwd()获取当前文件的所在位置
		self.__download_dir = downloadDir
		new_profile.set_preference("browser.download.dir", downloadDir)
		
		# 不管是 True 还是 False，都不显示开始，直接开始下载
		new_profile.set_preference('browser.download.manager.showWhenStarting', False)

		# selenium启动firefox时，设置浏览器的打开新窗口的方式：
		# 1 --- 在当前窗口打开，2 --- 新窗口方式打开，3 --- 以标签页方式打开
		new_profile.set_preference("browser.link.open_newwindow", 3)
		
		# browser.helperApps.neverAsk.saveToDisk 首选项值，应该是用逗号分隔的 MIME 类型列表
		# 参数为要下载页面的 Content-type 的值。指定哪些后缀名的文件，下载时就不询问直接下载。
		str_content_type = "text/html"
		for key in gContentTypeDic:
			str_content_type += ", " + gContentTypeDic[key]
		new_profile.set_preference("browser.helperApps.neverAsk.saveToDisk", str_content_type) 

		return new_profile


	def NewBrowser(self, browser_type, profile=None, bHeadless=True):
		""" 
		创建一个新的 Selenium 浏览器实例

		:参数:
		 - browser_type - 为 Selenium 指定一个所用浏览器，
		 可以是 "Chrome" 或 "Firefox"，字符串无视大小写

		 - profile - 当前浏览器所用配置
		 此配置无法在浏览器对象创建后，再重新修改
		 故必须在 SetBrowser() 调用前，先进行 profile 的设置

		 - bHeadless - 启动浏览器，是否为无界面模式
		 默认为无界面模式，为了查看效果可将此参数设为 False
		"""

		self.__browser_type = browser_type
		self.__headless = bHeadless
		
		if browser_type == "Firefox" or browser_type == "firefox":
			try:
				if self.__headless:
					options = webdriver.FirefoxOptions()
					options.set_headless()
					options.add_argument('--disable-gpu')
					self.__browser = webdriver.Firefox(firefox_profile=profile, firefox_options=options)
					self.__profile = profile
				else:
					self.__browser = webdriver.Firefox(firefox_profile=profile)
					self.__profile = profile
			except Exception as e:
				PrintException(Exception, e)
				raise
				return None
		else:
			print("不支持的浏览器类型")
			return None
			
		return self.__browser	


	def GetBrowserType(self):
		""" 返回浏览器类型，目前只有两种，Firefox 和 Chrome """
		return self.__browser_type


	def GetBrowser(self):
		""" 返回浏览器对象，类型为 webdriver.Firefox """
		return self.__browser


	def GetDownloadTmpSuffix(self):
		""" 返回当前浏览器下载临时文件的后缀名 """
		if self.__browser_type == "Firefox":
			return ""
		elif self.__browser_type == "Chrome":
			return ".crdownload"
		else:
			return ""
		

	def GetDownloadDir(self):
		""" 获取当前浏览器的文件下载路径 """
		return self.__download_dir


	def GetProfile(self):
		""" 返回浏览器配置，类型为 webdriver.FirefoxProfile """
		return self.__profile

	
	def SetWaitTime(self, waitsec):
		""" 设置打开一次浏览器等待时长 """
		self.__waitsec = waitsec


	def SetLogFileName(self, _name):
		""" 设置日志文件名 """
		self.__logfilename = _name


	def ClearInfo(self):
		""" 清空上一次查询的信息 """
		return


	def Reset(self):
		""" 重置浏览器，执行过程中发生异常时可调用 """
		self.ClearInfo()
		self.SetBrowser(self.__browser_type, self.__headless)


	def Close(self):
		""" 关闭当前正在运行的浏览器窗口 """
		self.ClearInfo()
		self.__browser.close()


	def Quit(self):
		""" 关闭所有浏览器窗口 """
		self.ClearInfo()
		self.__browser.quit()


	def GetElem(self, elem_name:str):
		""" 
		选择一个页面上的元素

		:参数:
		- elem_name: 页面上的元素/控件名称

		:返回:
		元素/控件对象。类型为
		selenium.webdriver.firefox.webelement.FirefoxWebElement
		"""
		elem = self.__browser.find_element_by_id(elem_name)
		return elem


	def Login(self, login_info:LoginInfo, timeout:int):
		""" 
		使用 Selenium 登陆
		
		:参数:
		- login_info:  登陆必须的参数，详见 LoginInfo 类说明
		- timeout:     登陆超时，单位：秒。保险起见最好设为8

		"""

		if login_info is None:
			return False
		if not login_info.IsOK():
			return False

		browser = self.__browser
		
		# 打开登陆页面
		self.Open_ByBrowser(login_info.loginUrl, timeout)
		# 通过使用选择器选择到表单元素进行模拟输入和点击按钮提交
		browser.find_element_by_id(login_info.elemUN).click()
		browser.find_element_by_id(login_info.elemUN).clear()
		browser.find_element_by_id(login_info.elemUN).send_keys(login_info.userName)
		browser.find_element_by_id(login_info.elemPWD).click()
		browser.find_element_by_id(login_info.elemPWD).clear()
		browser.find_element_by_id(login_info.elemPWD).send_keys(login_info.passWord)
		# 点击登陆
		browser.find_element_by_id(login_info.elemSUBMIT).click()

		# 输出 cookies 文件，方便做缓存登陆(不必每次都调用当前函数了)
		cookies = browser.get_cookies()
		self.ExportCookie_WgetUse(cookies)

		# 注意：返回成功仅仅是流程成功执行完，不意味着登陆一定就成功了。
		return True


	def Open_ByBrowser(self, url:str, timeout:int):
		""" 
		用浏览器打开某个Url，相当于把 url 粘贴到浏览器地址栏
		需要设置一个超时，以便后续的流程能继续执行。

		:参数:
		 - url - 要打开的 url 链接，必须是有效的字符串
		 - timeout - 超时时间，单位：秒，必须是 int 型

		:返回:
		 如果打开页面失败，返回一个空字符串。
		 如果成功打开页面，返回当前页面的源码
		"""
		ret_fail_str = ""

		if self.__browser is None or self.__profile is None:
			return ret_fail_str
		if str( type(url) ) != "<class 'str'>":
			print("Func Open_ByBrowser() Error: parameter url's type must be str ")
			return ret_fail_str
		if str( type(timeout) ) != "<class 'int'>":
			print("Func Open_ByBrowser() Error: parameter timeout's type must be int ")
			return ret_fail_str


		print("Please Wait " + str(timeout) + "s ...")
		self.__browser.set_page_load_timeout(timeout)
		try:
			self.__browser.get(url)
		except TimeoutException:
			pass
		finally:
			# 不管是否超时，都返回页面源码，以便进行后续分析
			# 为了能将页面 html 以 xml 格式进行分析，需要过滤掉一些不用的字符
			ret_str = self.__browser.page_source
			# list_body = re.findall("(<body)(.*?)(body>)", ret_str)
			# if list_body is None or len(list_body) < 3:
			# 	return ret_fail_str

			# ret_str = list_body[1] # 取出 <body> ... </body> 内容
			ret_str = ret_str.replace("&nbsp;", "")
			# ret_str = re.sub("(<img)(.*?)(>)", "", ret_str)
			return ret_str
		pass


	def Download_ByWget(self, url:str, filepath:str, logName:str):
		""" 
		用 wget 下载单个文件 

		:参数:
		- url - 文件下载链接
		- filepath - 包含文件名的完整路径，且该路径必须是个已存在的路径
		"""

		# 替换掉非法字符
		down_url = url
		down_filepath = filepath
		#down_filepath = CleanUpStr(filepath)

		# if down_url.find(u"\u") != -1:
		# 	pass
		# if down_filepath.find(u"\u") != -1:
		# 	pass

		# if down_url.find(u"\u3000") != -1:
		# 	down_url.replace(u"\u3000", "_")
		# if down_filepath.find(u"\u3000") != -1:
		# 	down_filepath.replace(u"\u3000", "_")

		cmd = "wget"
		cmd += " " + "--load-cookies cookies.txt"
		cmd += " " + "-c" 
		cmd += " " + "\"" + down_url + "\"" + " "
		#cmd += " " + "-O " + filepath
		cmd += " " + "-O " + "\"" + down_filepath + "\""

		# os.system(cmd)
		#str_ret = os.popen(cmd)

		# 这种方式替代直接执行 os.system() 没再出现下载慢，wget 崩溃的情况了。但还不知道什么原因
		sub2 = subprocess.Popen(cmd)
		pid_sub2 = sub2.pid
		while True:  
			# ret1 = subprocess.Popen.poll(sub2, stderr=subprocess.STDOUT)  
			ret1 = subprocess.Popen.poll(sub2)
			if ret1 == 0:  
				#print(sub2.pid, 'end')
				pass
				break  
			elif ret1 is None:  
				#print('running')

				# wget 子进程崩溃
				# if not sub2.is_alive():
				# 	self.AddLogToFile("wget was crashed", logName)
				# 	os.kill(pid_sub2, 0)

				time.sleep(1)  
			else:
				#print(sub2.pid,'term')
				pass
				break
		pass

		# try:
		# 	os.kill(pid_sub2, 0)
		# except OSError as err:
		# 	if err.errno == errno.ESRCH:
		# 		# ESRCH == No such process
		# 		return False
		# 	elif err.errno == errno.EPERM:
		# 		# EPERM clearly means there's a process to deny access to
		# 		return True
		# 	else:
		# 		# According to "man 2 kill" possible error values are
		# 		# (EINVAL, EPERM, ESRCH)
		# 		raise


	def ExportCookie_WgetUse(self, list_cookies:list, cookie_path:str="cookies.txt"):
		""" 
		导出成 wget 能用的 cookie.txt 文件。注意：
		1. 使用前确保你已经进入网站登录页并完成登陆
		2. 会覆盖之前的同名 cookie.txt 文件，调用前请确保没有同名文件

		:参数:
		- list_cookies: self.__browser.get_cookies() 的返回值 
		- cookie_path: 要输出的 cookie 文件名。

		格式例:
		- 账户名信息
		{
			'domain': 'www.baidu.com', 
			'expiry': None, 
			'httpOnly': False, 
			'name': 'ASPSESSIONIDQCDCDTRQ', 
			'path': '/', 
			'secure': False, 
			'value': 'GBPDPEFABLHIGLPLNJPJOCNL'
		}
		- 密码信息
		{
			'domain': 
			'www.17mol.com', 
			'expiry': None, 
			'httpOnly': False, 
			'name': 'KS%5FX1%2E5www17molcom', 
			'path': '/', 
			'secure': False, 
			'value': 'RndPassword=C%2DvI9xBO76pY7Vl4g2kp&GroupID=3&Password=ff2a17966bff75fa&UserName=%E9%98%BF%E6%8B%89%E4%BC%AF%E7%9A%84%E5%8A%B3%E4%BC%A6%E6%96%AF&UserID=15863'
		}
		"""

		info = ""
		info += "# Cookie File Formats:" + "\n"
		info += "# Domain" + "\t" + "HostOnly" + "\t" + "Path" + "\t" + \
		"Secure" + "\t" + "Expiry" + "\t" + "Name" + "\t" + "Value" + "\n"
		
		for dic_cookie in list_cookies:
			src_str_value = str(dic_cookie['value'])

			# 拆分原 value 字符串为字典，以便后续重新组合
			dict_value = {}
			if src_str_value.find("RndPassword") != -1:
				list_values = src_str_value.split("&")
				
				for str_val in list_values:
					cookieVals = str_val.split("=")
					cookieVal_key = cookieVals[0]
					cookieVal_value = cookieVals[1]
					dict_value[cookieVal_key] = cookieVal_value
					pass

				dst_str_value = ""
				dst_str_value += "UserName=" + dict_value["UserName"] + "&"
				dst_str_value += "UserID=" + dict_value["UserID"] + "&"
				dst_str_value += "GroupID=" + dict_value["GroupID"] + "&"
				dst_str_value += "Password=" + dict_value["Password"] + "&"
				dst_str_value += "AdminLoginCode=" + "&"
				dst_str_value += "ModelPower=" + "&"
				dst_str_value += "RndPassword=" + dict_value["RndPassword"] + "&"
				dst_str_value += "AdminPass=" + "&"
				dst_str_value += "PowerList=" + "&"
				dst_str_value += "AdminName=" + "&"
				dst_str_value += "SuperTF="
			else:
				dst_str_value = src_str_value


			info += str(dic_cookie['domain']) + "\t"
			info += "FALSE" + "\t"
			info += str(dic_cookie['path']) + "\t"
			info += str(dic_cookie['secure']).upper() + "\t"
			
			#info += str(dic_cookie['expiry']) + "\t"
			str_expiry = "0"
			if dic_cookie['expiry'] is not None:
				pass
			
			info += str_expiry + "\t"
			info += str(dic_cookie['name']) + "\t"
			info += dst_str_value + "\n"
			pass

		if os.path.exists(cookie_path):
			os.remove(cookie_path)

		outputstream = open(cookie_path, "a")
		outputstream.write(info)
		outputstream.close()

		pass


	# 目前没用上，先留着，有待完善。如果使用该方式，遇到需要登陆下载的情况，
	# 可能得先登陆获取 Cookie，这个还没研究......
	def Download_ByUrllib(self, _url:str, _file:str):
		""" 通过 url 下载文件，需要用到 urllib 库 """
		# 需要注意的地方：
		# (1) url 必须为 ASCII 编码的数据值，所以需要先用urllib.parse.quote() 转换一下
		#     注意 urllib.parse 需要单独 import
		# (2) 正确的做法是，只把原 url 中的中文做转换，其它不变。所以需要用到第二个参数 safe
		#     safe 表示我们不转换 url 中的哪些字符，我们需要对这些字符做过滤，
		#     以免它们也被一起转化了。如：“&”、“;” 等
		url_quote = urllib.parse.quote(_url, safe='/:?=;&')
		req = urllib.request.Request(url_quote)
		res = urllib.request.urlopen(req)

		# with open() 不必调用 close()
		file_bytes = res.read()
		with open(_file, 'wb') as f:
			f.write(file_bytes)



	def isNetOK(self):
		""" 检测网络连接是否正常 """
		s = socket.socket()
		s.settimeout(3)
		try:
			status = s.connect_ex( ("www.baidu.com", 443) )
			if status == 0:
				s.close()
				return True
			else:
				return False
		except Exception as e:
			print(e)
			return False


	def Exec(self, loginInfo:LoginInfo, LogicFunc, bLogin:bool=False, bShowBrowser:bool=True):
		""" 按流程执行爬虫程序 """
		logName = self.__logfilename

		if not self.isNetOK():
			print("Net is not OK !")
			sys.exit()

		self.WriteLogToFile("Now Create Profile ...", logName)

		# === 新建一个浏览器配置文件，阻止 Firefox 下载默认的弹窗 ===
		profile = self.NewProfile_Firefox()
		if profile is None:
			sys.exit()

		# === 新建一个浏览器对象实例 ===

		# 创建浏览器对象
		self.WriteLogToFile("Create Profile Success ! Now Create Browser ( ShowBrowser: " + str(bShowBrowser) +" )", logName)
		browser = self.NewBrowser("Firefox", profile, not bShowBrowser)
		self.WriteLogToFile("Now Create Browser. Please wait for a moment ......", logName)
		if browser is not None:

			''' 
			# 手动登陆，如果需要请打开该注释，并指定好所需的参数。
			if bLogin:
				self.Login(loginInfo, 8)
			'''

			self.WriteLogToFile("Create Browser Success ! Start Download Files ......", logName)
			try:
				LogicFunc(self)
			except Exception as e:
				PrintException(Exception, e)
				print("\n")
				return False
		
		# 程序结束统一处理
		return True
 